/*
  JCM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  JCM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with JCM.  If not, see <http://www.gnu.org/licenses/>.
*/
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.text.*;
import java.util.*;
import java.io.*;
import java.math.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.filechooser.FileFilter;
import javax.swing.plaf.*;
import javax.swing.undo.*;
import javax.print.*;
import javax.print.DocFlavor.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import com.toedter.calendar.JDateChooser;

/**
 * Implementação da interface do usuário e controles.
*/
public abstract class JCMFrame extends JFrame implements ActionListener, ChangeListener, FocusListener, ItemListener, PropertyChangeListener
{
  /** Construtor do view/controller do aplicativo.*/
  public JCMFrame()
  {
    super("Juros & Correção Monetária");

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    loadBehavior(); // carrega as propriedades da sessão anterior

    setLAF(LAFIndex); // reutiliza o Look&Feel da sessão anterior

    buildUI(); // construção da interface com o Look&Feel desejado

    createPopupMenus();

    // atualiza o submenu de seleção do Look&Feel efetivo
    LAFmenu.getItem(LAFIndex).setSelected(true);

    // atualiza o submenu gestor do tamanho de fonte
    enableFontMenuItens();

    popupListener = new TextFieldMouseAdapter();

    addPopupListener(getContentPane());

    if (soundEnabled) loadAudioClips();

    addWindowListener(new WindowAdapter() {
      @Override public void windowOpened(WindowEvent e)
      {
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();

        // tenta restaurar as dimensões do frame na sessão anterior
        if ((prevWidth <= 0) || (prevWidth > screen.width) || (prevHeight <= 0) || (prevHeight > screen.height))
        {
          prevHeight = 4 * 600 / 5;
          prevWidth = 4 * prevHeight / 3;
        }
        setSize(new Dimension(prevWidth, prevHeight));

        // tenta restaurar a posição do frame na sessão anterior
        if (prevX + getWidth() > screen.width) {
          prevX = screen.width - getWidth();
        }
        if (prevY + getHeight() > screen.height) {
          prevY = screen.height - getHeight();
        }
        setLocation(prevX, prevY);

        // tenta restaurar a posição do divisor de painéis sob restrições
        int min = (2 * formPane.getPreferredSize().width) / 3;
        int max = (2 * (split.getMaximumDividerLocation() - split.getDividerSize() - borderWidth)) / 3;
        // checa as restrições de posicionamento
        if (min < dividerLocation && dividerLocation < max) {
          // reposiciona o divisor conforme sessão anterior
          split.setDividerLocation(dividerLocation + borderWidth);
        } else {
          // usa a posição do divisor obtida na inicialização
          dividerLocation = split.getDividerLocation() - borderWidth;
        }

        // restaura o estado extendido do frame na sessão anterior
        setExtendedState(winState);

        // ações usuais do início de sessão
        vini.requestFocusInWindow();
        playAudioClip(0);

        // adiciona o listener de modificação do splitter por último
        // evitando conflito com a restauração do divisor de painéis
        split.addPropertyChangeListener((JCMFrame) e.getSource());
      }
      @Override public void windowIconified(WindowEvent e) { playAudioClip(9); }
      @Override public void windowDeiconified(WindowEvent e) { playAudioClip(10); }
      @Override public void windowClosing(WindowEvent e)
      {
        // máxima prioridade para o fechamento de conexão
        dbCloseConnection();
        // limpa as propriedades de escopo global e hereditário
        // reaproveitando o objeto no mecanismo de persistência
        properties.clear();
        saveBehavior();
      }
    });

    vini.setValue(BigDecimal.ZERO);
    vini.setFocusLostBehavior(JFormattedTextField.COMMIT);

    TAXA = new InterestRate(
      new BigDecimal(taxaSlider.getValue()).divide(DEZMIL, mc),
      periodCombo.getSelectedIndex(),
      opSimples.isSelected());
    taxa.setValue(TAXA.getValor().multiply(InterestRate.HUNDRED));
    taxa.setFocusLostBehavior(JFormattedTextField.COMMIT);
    taxa.addPropertyChangeListener("value", this);

    periodCombo.addFocusListener(this);
    periodCombo.addItemListener(this);

    indicesCombo.addFocusListener(this);
    indicesCombo.addItemListener(this);

    report.addFocusListener(this);

    loadNotes(introductionText);
  }

  /** Montagem da interface do usuário. */
  private void buildUI()
  {
    customizeUI();

    Font font = (Font) UIManager.getDefaults().get(defFontName);
    // checa se tamanho da fonte default é diferente do almejado
    if (font.getSize2D() != fontSize) {
      changeUIFontSizes(fontSize - font.getSize2D());
      font = font.deriveFont(fontSize);
    }

    // antecipa a criação do painel para obter a métrica da fonte
    JPanel valuePane = new JPanel();
    // obtêm a métrica da fonte
    FontMetrics metric = valuePane.getFontMetrics(font);
    // comprimento da letra 'M' em pixels :: conceito do CSS
    int hGap = metric.charWidth('M');
    // distância horizontal entre componentes
    int pad = hGap / 4;
    // quarta parte da altura padrão de uma linha em pixels
    int vPad = metric.getHeight() / 4;

    LightFocusTraversalPolicy policy = new LightFocusTraversalPolicy(19);

    /********    L E F T    S I D E    ********/

    JLabel label = new JLabel("Valor ($):", JLabel.RIGHT);
    label.setDisplayedMnemonic('V');

    vini = new JTextFieldDoubleEditor();
    vini.setColumns(10);
    vini.setName("VALOR");
    Insets padding = new Insets(1, 2, 1, 2);
    vini.setMargin(padding);
    vini.setHorizontalAlignment(JTextField.RIGHT);
    label.setLabelFor(vini);
    policy.add(vini);

    JPanel inner = new JPanel();
    inner.setLayout(new FlowLayout(FlowLayout.CENTER, pad, 0));
    inner.add(label);
    inner.add(vini);

    valuePane.setBorder(new TitledBorder(" Montante "));
    valuePane.setLayout(new BoxLayout(valuePane, BoxLayout.Y_AXIS));
    valuePane.add(Box.createVerticalGlue());
    valuePane.add(inner);
    valuePane.add(Box.createVerticalGlue());

    GridBagLayout gb = new GridBagLayout();
    GridBagConstraints gc = new GridBagConstraints();
    gc.fill = GridBagConstraints.VERTICAL;

    JPanel datasPane = new JPanel();
    datasPane.setBorder(new TitledBorder(" Período "));
    datasPane.setLayout(gb);

    Component stuff = Box.createVerticalStrut(vPad);
    gc.gridwidth = GridBagConstraints.REMAINDER;
    gc.weighty = 1/3d;
    gb.setConstraints(stuff, gc);
    datasPane.add(stuff);

    label = new JLabel("Início:");
    label.setDisplayedMnemonic('I');
    gc.anchor = GridBagConstraints.EAST;
    gc.gridwidth = 1;
    gc.insets.right = pad;
    gc.weighty = 0;
    gb.setConstraints(label, gc);
    datasPane.add(label);

    iniChooser = new JDateChooser();
    iniChooser.setLocale(DateUtils.localeBR);
    iniChooser.setDateFormatString("dd/MM/yyyy");
    iniChooser.getJCalendar().setWeekOfYearVisible(false);
    JTextComponent compo = (JTextComponent) iniChooser.getDateEditor().getUiComponent();
    compo.setName("INIDATE");
    compo.setMargin(padding);
    label.setLabelFor(compo);
    policy.add(compo);
    iniChooser.getCalendarButton().setName("INIDATEBTN");
    policy.add(iniChooser.getCalendarButton());
    gc.anchor = GridBagConstraints.WEST;
    gc.gridwidth = GridBagConstraints.REMAINDER;
    gc.insets.right = 0;
    gc.ipadx = hGap;
    gc.ipady = vPad;
    gb.setConstraints(iniChooser, gc);
    datasPane.add(iniChooser);

    stuff = Box.createVerticalStrut(vPad);
    gc.anchor = GridBagConstraints.CENTER;
    gc.ipadx = gc.ipady = 0;
    gc.weighty = 1/3d;
    gb.setConstraints(stuff, gc);
    datasPane.add(stuff);

    label = new JLabel("Fim:");
    label.setDisplayedMnemonic('F');
    gc.anchor = GridBagConstraints.EAST;
    gc.gridwidth = 1;
    gc.insets.right = pad;
    gc.weighty = 0;
    gb.setConstraints(label, gc);
    datasPane.add(label);

    fimChooser = new JDateChooser();
    fimChooser.setLocale(DateUtils.localeBR);
    fimChooser.setDateFormatString("dd/MM/yyyy");
    fimChooser.getJCalendar().setWeekOfYearVisible(false);
    compo = (JTextComponent) fimChooser.getDateEditor().getUiComponent();
    compo.setName("ENDDATE");
    compo.setMargin(padding);
    label.setLabelFor(compo);
    policy.add(compo);
    fimChooser.getCalendarButton().setName("ENDDATEBTN");
    policy.add(fimChooser.getCalendarButton());
    gc.anchor = GridBagConstraints.WEST;
    gc.gridwidth = GridBagConstraints.REMAINDER;
    gc.insets.right = 0;
    gc.ipadx = hGap;
    gc.ipady = vPad;
    gb.setConstraints(fimChooser, gc);
    datasPane.add(fimChooser);

    stuff = Box.createVerticalStrut(vPad);
    gc.anchor = GridBagConstraints.CENTER;
    gc.ipadx = gc.ipady = 0;
    gc.weighty = 1/3d;
    gb.setConstraints(stuff, gc);
    datasPane.add(stuff);

    taxaSlider = new JSlider(JSlider.HORIZONTAL, 0, 1200, 100);
    taxaSlider.setName("SLIDER");
    taxaSlider.setBorder(BorderFactory.createEmptyBorder(0, pad, pad, pad));
    font = deriveSliderFont(font);
    Dictionary<Integer,JLabel> h = new Hashtable<Integer,JLabel>();
    for (int j=0; j <= 12; j++) {
      label = new JLabel(((j % 2) == 0) ? String.valueOf(j) : "");
      label.setFont(font);
      h.put(new Integer(j * 100), label);
    }
    taxaSlider.setLabelTable(h);
    taxaSlider.setMinorTickSpacing(20);
    taxaSlider.setMajorTickSpacing(100);
    taxaSlider.setSnapToTicks(false);
    taxaSlider.setPaintTicks(true);
    taxaSlider.setPaintLabels(true);
    taxaSlider.setPaintTrack(true);
    taxaSlider.addChangeListener(this);
    policy.add(taxaSlider);

    label = new JLabel("Taxa:", JLabel.RIGHT);
    label.setDisplayedMnemonic('T');

    taxa = new JTextFieldDoubleEditor();
    taxa.setColumns(6);
    taxa.setName("TAXA");
    taxa.setMargin(padding);
    taxa.setHorizontalAlignment(JTextField.RIGHT);
    label.setLabelFor(taxa);
    policy.add(taxa);

    periodCombo = new JComboBox();
    periodCombo.setRenderer(new ComboCellRenderer());
    periodCombo.setName("PERIODO");
    periodCombo.addItem("% ao dia");
    periodCombo.addItem("% ao mês");
    periodCombo.addItem("% ao ano");
    periodCombo.setSelectedIndex(1);
    policy.add(periodCombo);

    txPane = new JPanel();
    txPane.setLayout(new FlowLayout(FlowLayout.CENTER, pad, 0));
    txPane.add(label);
    txPane.add(taxa);
    txPane.add(periodCombo);

    opSimples = new JRadioButton("simples", true);
    opSimples.setMnemonic('M');
    opSimples.setAlignmentX(Component.CENTER_ALIGNMENT);
    opSimples.addItemListener(this);

    opComposta = new JRadioButton("composta", false);
    opComposta.setMnemonic('O');
    opComposta.setAlignmentX(Component.CENTER_ALIGNMENT);
    opComposta.addItemListener(this);

    regimeGroup = new ButtonGroup();
    regimeGroup.add(opSimples);
    regimeGroup.add(opComposta);

    policy.add(opSimples);
    policy.add(opComposta);

    inner = new JPanel();
    inner.setLayout(new BoxLayout(inner, BoxLayout.X_AXIS));
    inner.add(Box.createHorizontalGlue());
    inner.add(opSimples);
    inner.add(Box.createHorizontalGlue());
    inner.add(opComposta);
    inner.add(Box.createHorizontalGlue());

    JPanel regimePane = new JPanel();
    regimePane.setName("REGIME");
    regimePane.setBorder(new TitledBorder(" Capitalização "));
    regimePane.setLayout(new BoxLayout(regimePane, BoxLayout.Y_AXIS));
    regimePane.add(Box.createVerticalGlue());
    regimePane.add(inner);
    regimePane.add(Box.createVerticalGlue());

    opSemestral = new JRadioButton("semestral", false);
    opSemestral.setMnemonic('E');
    opSemestral.setAlignmentX(Component.CENTER_ALIGNMENT);
    opSemestral.addItemListener(this);

    opAnual = new JRadioButton("anual", true);
    opAnual.setMnemonic('U');
    opAnual.setAlignmentX(Component.CENTER_ALIGNMENT);
    opAnual.addItemListener(this);

    agregacaoGroup = new ButtonGroup();
    agregacaoGroup.add(opSemestral);
    agregacaoGroup.add(opAnual);

    policy.add(opSemestral);
    policy.add(opAnual);

    inner = new JPanel();
    inner.setLayout(new BoxLayout(inner, BoxLayout.X_AXIS));
    inner.add(Box.createHorizontalGlue());
    inner.add(opSemestral);
    inner.add(Box.createHorizontalGlue());
    inner.add(opAnual);
    inner.add(Box.createHorizontalGlue());

    JPanel agregacaoPane = new JPanel();
    agregacaoPane.setName("AGREGACAO");
    agregacaoPane.setBorder(new TitledBorder(" Agregação "));
    agregacaoPane.setLayout(new BoxLayout(agregacaoPane, BoxLayout.Y_AXIS));
    agregacaoPane.add(Box.createVerticalGlue());
    agregacaoPane.add(inner);
    agregacaoPane.add(Box.createVerticalGlue());

    JPanel ratePane = new JPanel();
    ratePane.setBorder(new TitledBorder(" Juros "));
    ratePane.setLayout(new BoxLayout(ratePane, BoxLayout.Y_AXIS));
    ratePane.add(Box.createVerticalGlue());
    ratePane.add(taxaSlider);
    ratePane.add(Box.createVerticalGlue());
    ratePane.add(txPane);
    ratePane.add(Box.createVerticalGlue());
    ratePane.add(regimePane);
    ratePane.add(agregacaoPane);

    label = new JLabel("Índice Mensal:", JLabel.RIGHT);
    label.setDisplayedMnemonic('d');

    indicesCombo = new JComboBox();
    indicesCombo.setRenderer(new ComboCellRenderer());
    indicesCombo.setName("INDICES_COMBO");
    label.setLabelFor(indicesCombo);
    policy.add(indicesCombo);

    inner = new JPanel();
    inner.setLayout(new FlowLayout(FlowLayout.CENTER, pad, 0));
    inner.add(label);
    inner.add(indicesCombo);

    JPanel indicesPane = new JPanel();
    indicesPane.setName("INDICES_PANE");
    indicesPane.setBorder(new TitledBorder(" Correção Monetária "));
    indicesPane.setLayout(new BoxLayout(indicesPane, BoxLayout.Y_AXIS));
    indicesPane.add(Box.createVerticalGlue());
    indicesPane.add(inner);
    indicesPane.add(Box.createVerticalGlue());

    // dimensão inicial do formulário para resolução mínima de 800x600
    // altura <- 75% da altura do screen
    int height = 3 * 600 / 4;
    // proporção entre altura e largura igual a constante da secção áurea
    int width = (int) (height / ((1 + Math.sqrt(5d)) / 2));

    formPane = new JPanel();
    // dimensão zero para que o painel possa encolher completamente
    formPane.setMinimumSize(new Dimension(0, 0));
    // declara a dimensão base
    formPane.setSize(new Dimension(width, height));
    formPane.setBorder(BorderFactory.createEmptyBorder(pad, pad, pad, pad));
    formPane.setLayout(gb);
    gc.fill = GridBagConstraints.BOTH;
    gc.weightx = 1;
    gc.weighty = 0.125d;
    gb.setConstraints(valuePane, gc);
    gb.setConstraints(indicesPane, gc);
    gb.setConstraints(datasPane, gc);
    gc.weighty = 1 - 3 * gc.weighty;
    gb.setConstraints(ratePane, gc);
    formPane.add(valuePane);
    formPane.add(datasPane);
    formPane.add(ratePane);
    formPane.add(indicesPane);

    /********    R I G H T    S I D E    ********/

    report = new JTextPane();
    report.setEditable(false);
    report.setBorder(
      BorderFactory.createLineBorder(Color.decode("0xFDFDFD"), 1));

    displayPane = new JScrollPane(report);
    // dimensão zero para que o painel possa encolher completamente
    displayPane.setMinimumSize(new Dimension(0, 0));
    // declara a dimensão base
    displayPane.setSize(new Dimension(height, height));
    displayPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    displayPane.setBorder(
      new CompoundBorder(
        BorderFactory.createEmptyBorder(pad, pad, pad, pad),
        new CompoundBorder(
          new CompoundBorder(
            new TitledBorder(" Resultados "),
            BorderFactory.createEmptyBorder(0, pad, pad, pad)),
          displayPane.getBorder())));

    /**********    S P L I T   P A N E L    **********/

    split = new JSplitPane(
      JSplitPane.HORIZONTAL_SPLIT, formPane, displayPane);
    split.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));
    // permite uso do widget de posicionamento
    split.setOneTouchExpandable(true);
    // inibe renderização durante ajuste do divisor via dragagem
    split.setContinuousLayout(false);
    // permite foco para ajuste do divisor pelo teclado
    split.setFocusable(true);

    /***** A C T I O N   P A N E L *****/

    progressBar = new JProgressBar(0, 100);
    progressBar.setString("AGUARDE POR FAVOR");
    progressBar.setStringPainted(false);
    progressBar.setBorderPainted(false);

    properties = loadProperties("img.properties");

    // carrega o ícone do aplicativo
    setIconImage(loadImageIcon(peek(properties, "icoApp")).getImage());

    execBtn = new JButton("Calcular");
    execBtn.setName("EXEC_BTN");
    execBtn.setMnemonic(KeyEvent.VK_C);
    execBtn.setIcon(loadImageIcon(peek(properties, "icoExeDef")));
    execBtn.setRolloverIcon(loadImageIcon(peek(properties, "icoExeOvr")));

    saveBtn = new JButton("Salvar");
    saveBtn.setName("SAVE_BTN");
    saveBtn.setMnemonic(KeyEvent.VK_S);
    saveBtn.setIcon(loadImageIcon(peek(properties, "icoSavDef")));
    saveBtn.setRolloverIcon(loadImageIcon(peek(properties, "icoSavOvr")));

    printBtn = new JButton("Imprimir");
    printBtn.setName("PRINT_BTN");
    printBtn.setMnemonic(KeyEvent.VK_P);
    printBtn.setIcon(loadImageIcon(peek(properties, "icoPrnDef")));
    printBtn.setRolloverIcon(loadImageIcon(peek(properties, "icoPrnOvr")));

    aboutBtn = new JButton("Sobre");
    aboutBtn.setName("ABOUT_BTN");
    aboutBtn.setMnemonic(KeyEvent.VK_B);
    aboutBtn.setIcon(loadImageIcon(peek(properties, "icoAboutDef")));
    aboutBtn.setRolloverIcon(loadImageIcon(peek(properties, "icoAboutOvr")));

    JPanel butPane = new JPanel();
    butPane.setLayout(new GridLayout(1, 4, pad, 0));
    butPane.add(execBtn);
    butPane.add(saveBtn);
    butPane.add(printBtn);
    butPane.add(aboutBtn);

    padding = new Insets(pad, pad, pad, pad);
    for (Component c: butPane.getComponents())
    {
      JButton b = (JButton) c;
      b.setMargin(padding);
      b.addActionListener(this);
      policy.add(b);
    }

    policy.add(report);
    policy.add(split);

    actionPane = new JPanel();
    actionPane.setBorder(
      BorderFactory.createEmptyBorder(pad, pad, pad, pad));
    actionPane.setLayout(new BorderLayout(pad, 0));
    actionPane.add(progressBar, BorderLayout.CENTER);
    actionPane.add(butPane, BorderLayout.EAST);

    setFocusTraversalPolicy(policy);

    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(split, BorderLayout.CENTER);
    getContentPane().add(actionPane, BorderLayout.SOUTH);

    // calcula a espessura das bordas do Splitter
    getBorderWidth();
    // posiciona o divisor de painéis evitando quebra de layout
    adjustSplitter();
  }

  /** Item de menu especializado para seleção de Look&Feel. */
  private class LAFMenuItem extends JRadioButtonMenuItem
  {
    LAFMenuItem(String text)
    {
      super(text);
      super.setOpaque(false);
    }

    /** Índice do Look&Feel acionado por esse item de menu. */
    private int index;

    /** Associa o índice do Look&Feel ao item de menu. */
    public void setIndex(int aIndex) { index = aIndex; }

    /** Retorna o índice do Look&Feel associado ao item de menu. */
    public int getIndex() { return index; }

    static final long serialVersionUID = -2881051410953416352L;
  }

  /** Cria todos os menus popup do aplicativo. */
  private void createPopupMenus()
  {
    mainMenu = new JPopupMenu();
    mainMenu.setOpaque(false);
    mainMenu.setBorder(
      new LogoBorder(
        loadImageIcon(peek(properties, "icoAppMenu")).getImage(), null, true));
    mainMenu.setLightWeightPopupEnabled(true);

    properties = loadProperties("tooltiptext.properties");

    JMenu submenu = new JMenu("Exibir Painel");

      submenu.getPopupMenu().setOpaque(false);
      submenu.getPopupMenu().setBorder(new TransparentBorder());
      submenu.setMnemonic('P');

      JMenuItem item = buildMenuItem("Resultados", "SHOW_RESULTS", 'S', KeyStroke.getKeyStroke("alt 1"));
      submenu.add(item);

      item = buildMenuItem("Simultâneos", "SHOW_BOTH", 'U', KeyStroke.getKeyStroke("alt 2"));
      item.setEnabled(false);
      submenu.add(item);

    mainMenu.add(splitMenu = submenu);

    submenu = new JMenu("Banco de Dados");

      submenu.getPopupMenu().setOpaque(false);
      submenu.getPopupMenu().setBorder(new TransparentBorder());
      submenu.setName("DB_MENU");
      submenu.setMnemonic('B');

      item = buildMenuItem("Análise Operacional", "DBREPORT", 'O', null);
      submenu.add(item);

      submenu.addSeparator();

      item = buildMenuItem("Atualizar Tabela", "UPDATE_TABLE", 'A', null);
      submenu.add(item);

      submenu.addSeparator();

      item = buildMenuItem("Editar Tabela", "EDIT_TABLE", 'E', null);
      submenu.add(item);

      item = buildMenuItem("Criar Tabela", "CREATE_TABLE", 'C', null);
      submenu.add(item);

      item = buildMenuItem("Eliminar Tabela", "DROP_TABLE", 'U', null);
      submenu.add(item);

    mainMenu.add(submenu);

    mainMenu.addSeparator();

    JCheckBoxMenuItem chkMenuItem = buildCheckMenuItem("Datas Lenientes", "LENIENT", 'L', null, lenientDatesEnabled);
    mainMenu.add(chkMenuItem);

    chkMenuItem = buildCheckMenuItem("Taxas Equivalentes", "SCALE", 'E', null, autoScaleEnabled);
    mainMenu.add(chkMenuItem);

    mainMenu.addSeparator();

    chkMenuItem = buildCheckMenuItem("Ajuda Contextual", "TOOLTIP", 'C', null, toolTipManager.isEnabled());
    mainMenu.add(chkMenuItem);

    chkMenuItem = buildCheckMenuItem("Efeitos Sonoros", "SOUND", 'F', null, soundEnabled);
    mainMenu.add(chkMenuItem);

    chkMenuItem = buildCheckMenuItem("Advertências", "WARNINGS", 'V', null, warningsEnabled);
    mainMenu.add(chkMenuItem);

    chkMenuItem = buildCheckMenuItem("CA do BCB", "CACERTS", 'J', null, checkCAcerts);
    mainMenu.add(chkMenuItem);

    mainMenu.addSeparator();

    fontMenu = new JMenu("Fonte");

      fontMenu.getPopupMenu().setOpaque(false);
      fontMenu.getPopupMenu().setBorder(new TransparentBorder());
      fontMenu.setMnemonic('F');

      item = buildMenuItem("Aumentar", "INCFONT", 'A', null);
      fontMenu.add(item);

      item = buildMenuItem("Reduzir", "DECFONT", 'R', null);
      fontMenu.add(item);

      fontMenu.addSeparator();

      chkMenuItem = buildCheckMenuItem("Negrito", "BOLD", 'N', null, boldEnabled);
      fontMenu.add(chkMenuItem);

    mainMenu.add(fontMenu);

    LAFmenu = new JMenu("Look&Feel");

      LAFmenu.getPopupMenu().setOpaque(false);
      LAFmenu.getPopupMenu().setBorder(new TransparentBorder());
      LAFmenu.setName(LAFPrefix);
      LAFmenu.setMnemonic('K');

      ButtonGroup group = new ButtonGroup();
      for (UIManager.LookAndFeelInfo info: LAFInfo)
      {
        LAFMenuItem mi = new LAFMenuItem( info.getName() );
        mi.setIndex( LAFmenu.getItemCount() );
        mi.addActionListener(this);
        group.add(mi);
        LAFmenu.add(mi);
      }

    mainMenu.add(LAFmenu);

    submenu = new JMenu("Informações");

      submenu.getPopupMenu().setOpaque(false);
      submenu.getPopupMenu().setBorder(new TransparentBorder());
      submenu.setMnemonic('I');

      item = buildMenuItem("Introdução", "TIPS", 'D', null);
      submenu.add(item);

      item = buildMenuItem("Licença", "LIC", 'N', null);
      submenu.add(item);

    mainMenu.add(submenu);

    item = buildMenuItem("Sair", "EXIT" ,'R', KeyStroke.getKeyStroke("control Q"));
    mainMenu.add(item);

    editMenu = new JPopupMenu();
    editMenu.setLightWeightPopupEnabled(true);
    editMenu.setOpaque(false);
    editMenu.setBorder(new TransparentBorder());

    item = buildMenuItem("Copiar", "COPY", null, KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK));
    editMenu.add(item);

    item = buildMenuItem("Cortar", "CUT", null, KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_MASK));
    editMenu.add(item);

    item = buildMenuItem("Colar", "PASTE", null, KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK));
    editMenu.add(item);

    editMenu.addSeparator();

    item = buildMenuItem("Selecionar Tudo", "SELECT_ALL", null, KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_MASK));
    editMenu.add(item);

    editMenu.addSeparator();

    item = buildMenuItem("Desfazer", "UNDO", null, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_MASK));
    editMenu.add(item);

    item = buildMenuItem("Refazer", "REDO", null, KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK));
    editMenu.add(item);

    editMenu.addPopupMenuListener(
      new PopupMenuListener()
      {
        /** tipo de dado preferido nos input fields */
        final DataFlavor dataFlavor = new DataFlavor(String.class, "text/plain");

        /**
         * Habilitação dos itens do menu de edição.
         *
         * @param e Evento popup no momento anterior a visibilidade.
        */
        public void popupMenuWillBecomeVisible(PopupMenuEvent e)
        {
          // obtêm todos os itens replicantes do menu de edição
          MenuElement[] itens = editMenu.getSubElements();

          // acessa o componente que originou o evento
          JTextComponent field = (JTextComponent) popupListener.originator;

          // acessa o texto selecionado se possível
          String text = field.getSelectedText();

          // habilita o item COPY se há texto selecionado
          boolean b = (text != null) && (text.length() > 0);
          ((JMenuItem) itens[0]).setEnabled(b);

          // habilita o item CUT se há texto selecionado
          ((JMenuItem) itens[1]).setEnabled(b);

          // habilita o item SELECT_ALL se há texto não selecionado
          b = (field.getText().length() != (b ? text.length() : 0));
          ((JMenuItem) itens[3]).setEnabled(b);

          // tenta obter o conteúdo do clipboard no tipo preferido
          try {
            text = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(dataFlavor);
          } catch (UnsupportedFlavorException ue) {
            // System.err.println(ue);
            text = null;
          } catch (IOException ie) {
            // System.err.println(ie);
            text = null;
          }

          // habilita o item PASTE se há texto no clipboard
          b = (text != null) && (text.length() > 0);
          ((JMenuItem) itens[2]).setEnabled(b);

          UndoManager undoManager = undoManagerList.get(field.getName());

          // habilita o item UNDO
          b = undoManager.canUndo();
          ((JMenuItem) itens[4]).setEnabled(b);

          // habilita o item REDO
          b = undoManager.canRedo();
          ((JMenuItem) itens[5]).setEnabled(b);
        }

        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {}

        public void popupMenuCanceled(PopupMenuEvent e) {}
      });
  }

  /**
   * Preenche as propriedades de componente item de menu.
   *
   * @param item Item de menu a preencher.
   * @param name Nome do componente.
   * @param mnemonic Caractere mnemônico para acionar o item via <i>Alt</i>.
   * @param keyStroke Combinação de teclas para acionar o item.
   * @return Item de menu preenchido.
  */
  private JMenuItem fillMenuItem(JMenuItem item, String name, Character mnemonic, KeyStroke keyStroke)
  {
    item.setName(name);
    if (mnemonic != null) item.setMnemonic(mnemonic);
    if (keyStroke != null) item.setAccelerator(keyStroke);
    item.setOpaque(false);
    addToolTip(item);
    item.addActionListener(this);
    return item;
  }

  /**
   * Constrói item de menu assinalando valor de propriedades.
   *
   * @param text Texto de interface ao usuário.
   * @param name Nome do componente.
   * @param mnemonic Caractere mnemônico para acionar o item via <i>Alt</i>.
   * @param keyStroke Combinação de teclas para acionar o item.
   * @return Item de menu.
  */
  private JMenuItem buildMenuItem(String text, String name, Character mnemonic, KeyStroke keyStroke)
  {
    return fillMenuItem(new JMenuItem(text), name, mnemonic, keyStroke);
  }

  /**
   * Constrói item checkbox de menu assinalando valor de propriedades.
   *
   * @param text Texto de interface ao usuário.
   * @param name Nome do componente.
   * @param mnemonic Caractere mnemônico para acionar o item via <i>Alt</i>.
   * @param keyStroke Combinação de teclas para acionar o item.
   * @param state Estado inicial do item <b>true</b> para marcado
   *              <b>false</b> em caso contrário.
   * @return Item de menu.
  */
  private JCheckBoxMenuItem buildCheckMenuItem(String text, String name, Character mnemonic, KeyStroke keyStroke, boolean state)
  {
    return (JCheckBoxMenuItem) fillMenuItem(
      new JCheckBoxMenuItem(text, state), name, mnemonic, keyStroke);
  }

  /**
   * Adiciona texto de ajuda contextual ao componente visual como possível.
   *
   * @param c Componente visual objeto da operação.
   */
  private void addToolTip(JComponent c)
  {
    if (c.getName() != null) {
      c.setToolTipText(properties.getProperty(c.getName()));
    }
  }

  /**
   * Recursivamente adiciona listener de eventos do mouse aos componentes
   * filhos do container e também adiciona ações aos mesmos se forem do
   * tipo texto assim como tenta adicionar texto de ajuda contextual para
   * cada componente visualizável.
   *
   * @param parent Container pai objeto da operação.
  */
  private void addPopupListener(Container parent)
  {
    for (Component child: parent.getComponents())
      // checa se o componente é visualizável
      if (child.isVisible())
      {
        // adiciona o popup listener ao componente
        child.addMouseListener(popupListener);
        // capacita UNDO/REDO se componente destina-se a entrada de texto
        if (child instanceof JTextComponent) addUndoRedoPopup((JTextComponent) child);
        // tenta declarar o texto contextual do componente
        if (child instanceof JComponent) addToolTip((JComponent) child);
        // tenta adicionar o popup listener aos filhos do componente
        addPopupListener((Container) child);
      }
  }

  /**
   * Associa ações a teclas para Fazer/Refazer edições de texto, acionar
   * o menu popup de edição e encerrar a edição de campos de textos
   * formatados.
   *
   * @param component Componente objeto da associação de ações a teclas.
  */
  private void addUndoRedoPopup(JTextComponent component)
  {
    if (component.isEditable())
    {
      if (undoManagerList == null) {
        // inicializa a lista de gestores de históricos de edição
        undoManagerList = new Hashtable<String,UndoManager>(4);
      }
      // instancia um novo gestor de histórico de edição
      final UndoManager undo = new UndoManager();
      // alista o par nome do componente e gestor de histórico de edição
      // para facilitar acesso futuro
      undoManagerList.put(component.getName(), undo);

      Document doc = component.getDocument();

      doc.addUndoableEditListener(
        new UndoableEditListener() {
          public void undoableEditHappened(UndoableEditEvent uev) {
            undo.addEdit(uev.getEdit());
          }
        });

      ActionMap actionMap = component.getActionMap();
      InputMap inputMap = component.getInputMap();

      String actionName = "UNDO";
      actionMap.put(actionName,
        new AbstractAction(actionName) {
          public void actionPerformed(ActionEvent evt) {
            try {
              if (undo.canUndo()) undo.undo();
            } catch (CannotUndoException e) {
            }
          }
          private static final long serialVersionUID = 1540006323401207543L;
        });
      inputMap.put(KeyStroke.getKeyStroke("control Z"), actionName);

      actionName = "REDO";
      actionMap.put(actionName,
        new AbstractAction("REDO") {
          public void actionPerformed(ActionEvent evt) {
            try {
              if (undo.canRedo()) undo.redo();
            } catch (CannotRedoException e) {
            }
          }
          private static final long serialVersionUID = 2692398611040286825L;
        });
      inputMap.put(KeyStroke.getKeyStroke("control Y"), actionName);

      if (component instanceof JFormattedTextField) {
        actionName = "DO_COMMIT";
        actionMap.put(actionName,
          new AbstractAction(actionName) {
            public void actionPerformed(ActionEvent evt) {
              JFormattedTextField t = (JFormattedTextField) evt.getSource();
              try {
                t.commitEdit();
                Object value = t.getValue();
                if (value != null) {
                  t.setText(t.getFormatter().valueToString((Number) value));
                }
              } catch (ParseException e) {
              }
            }
            private static final long serialVersionUID = 8385506498940231617L;
          });
        inputMap.put(KeyStroke.getKeyStroke("ENTER"), actionName);
      }

      actionName = "EDIT_POPUP";
      actionMap.put(actionName,
        new AbstractAction(actionName) {
          public void actionPerformed(ActionEvent evt) {
            JComponent c = (JComponent) evt.getSource();
            // preserva o componente origem do evento
            popupListener.originator = c;
            // abre o menu popup de edição em posição relativa
            // à dimensão do componente
            editMenu.show(c, c.getWidth() / 2, c.getHeight() * 3 / 4);
          }
          private static final long serialVersionUID = 478603381899834946L;
        });
      inputMap.put(KeyStroke.getKeyStroke("F10"), actionName);
    }
  }

  /**
   * Cria um componente JRootPane associando a tecla F12 a ação
   * de abrir o menu popup principal centralizado na janela.
   *
   * @return Instância de componente JRootPane especializada.
  */
  @Override protected JRootPane createRootPane()
  {
    JRootPane root = new JRootPane();
    String actionName = "MAIN_POPUP";
    Action action = new AbstractAction()
    {
      public void actionPerformed(ActionEvent ae)
      {
        int x = mainMenu.getWidth();
        int y = mainMenu.getHeight();
        // workaround to the bug of unknown dimension on first invocation
        if (x == 0 && y == 0) {
          mainMenu.show(getContentPane(), 0, 0);
          mainMenu.setVisible(false);
          // obtain the right dimension
          x = mainMenu.getWidth();
          y = mainMenu.getHeight();
        }
        x = (getWidth() - x) / 2;
        y = (getHeight() - y) / 2;
        mainMenu.show(getContentPane(), x, y);
      }
      private static final long serialVersionUID = 1122144258733653757L;
    };
    root.getActionMap().put(actionName, action);
    InputMap inputMap = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    inputMap.put(KeyStroke.getKeyStroke("F12"), actionName);
    return root;
  }

  /**
   * Carrega imagem como resource.
   *
   * @param filename Nome do arquivo de resource contendo a imagem.
  */
  private ImageIcon loadImageIcon(String filename)
  {
    return new ImageIcon(getClass().getClassLoader().getResource(filename));
  }

  /** Carrega audioclips como resource. */
  private void loadAudioClips()
  {
    // carrega os audioclips como resources
    EventQueue.invokeLater(new Runnable() {
      public void run()
      {
        Properties p = loadProperties("snd.properties");
        audioclip = new AudioClip[p.size()];
        for (int j=0; j < audioclip.length; j++) {
          String filename = p.getProperty(String.format("SND%02d", j));
          if (filename != null) {
            audioclip[j] = Applet.newAudioClip(getClass().getResource(filename));
          }
        }
      }
    });
  }

  /***** SUPPORT METHODS *****/

  /**
   * Atualiza recursivamente a interface visual do menu popup.
   *
   * @param popmenu Menu popup objeto da atualização.
  */
  private void updatePopupMenuUI(JPopupMenu popmenu)
  {
    popmenu.updateUI();
    for (MenuElement m: popmenu.getSubElements())
    {
      ((JMenuItem) m).updateUI();
      if (m instanceof JMenu) updatePopupMenuUI(((JMenu) m).getPopupMenu());
    }
  }

  /**
   * Assinala como Look&Feel efetivo o que corresponde ao de índice
   * fornecido no array de informações de sua classe.
   *
   * @param index Índice do Look&Feel no array de informações de sua classe.
  */
  private void setLAF(int index)
  {
    // classname do Look&Feel efetivo
    String LAFClassname = UIManager.getLookAndFeel().getClass().getName();
    // checa se o Look&Feel pretendido não é o efetivo
    if (! LAFInfo[index].getClassName().equals(LAFClassname))
    {
      final long DELAY = 100L;
      int state = 0, w = 0, h = 0;
      Point at = null;
      // checa se está alterando Look&Feel por iniciativa do usuário
      if (isVisible()) {
        //
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        // preserva o estado extendido do frame
        state = getExtendedState();
        // se o frame está maximizado então altera para estado normal
        if ((state & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) {
          setExtendedState(Frame.NORMAL);
        }
        // obtêm a largura do screen
        w = Toolkit.getDefaultToolkit().getScreenSize().width;
        // espera até que o frame esteja no estado normal
        try {
          do Thread.sleep(DELAY); while (getWidth() == w);
        } catch (InterruptedException e) {
          System.err.println(e);
        }
        // obtêm as dimensões do frame no estado normal
        w = getWidth();
        h = getHeight();
        // obtêm a posição do frame no screen em estado normal
        at = getLocationOnScreen();
      }

      // tenta alterar o Look&Feel efetivo
      try {
        UIManager.setLookAndFeel( LAFInfo[index].getClassName() );
      } catch (IllegalAccessException e) {
        System.err.println(e);
      } catch (InstantiationException e) {
        System.err.println(e);
      } catch (ClassNotFoundException e) {
        System.err.println(e);
      } catch (UnsupportedLookAndFeelException e) {
        System.err.println(e);
      }

      // tenta atualizar todos os componentes visuais
      try {
        SwingUtilities.updateComponentTreeUI(this);
      } catch (Exception e) {
        System.err.println(e);
        // tenta atualizar o último dos painéis
        try {
          SwingUtilities.updateComponentTreeUI(actionPane);
        } catch (Exception ex) {
          System.err.println(ex);
        }
      }

      if (isVisible())
      {
        // ajusta dimensões conforme novo Look&Feel efetivo
        validate();

        // calcula a espessura das bordas do Splitter
        getBorderWidth();
        // posiciona o divisor de painéis evitando quebra de layout
        adjustSplitter();

        // atualiza as interfaces dos menus
        updatePopupMenuUI(mainMenu);
        updatePopupMenuUI(editMenu);

        // tenta restaurar a dimensão no estado normal
        setSize(new Dimension(w, h));
        try { // espera pelo redimensionamento
          do Thread.sleep(DELAY); while (getWidth() != w);
        } catch (InterruptedException e) {
          System.err.println(e);
        }

        // tenta restaurar a posição da janela
        setLocation(at);
        try { // espera pelo reposicionamento
          do Thread.sleep(DELAY); while (! getLocationOnScreen().equals(at));
        } catch (InterruptedException e) {
          System.err.println(e);
        }

        // se necessário restaura a maximização evitando falha de sincronia
        if ((state & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) {
          EventQueue.invokeLater(new Runnable() {
            public void run() { setExtendedState(Frame.MAXIMIZED_BOTH); }
          });
        }

        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

        // adverte o usuário sobre a necessidade de reiniciar o aplicativo
        if (warningsEnabled) {
          String msg = String.format("<html>O Look&Feel <b>%s</b> foi aplicado.<br>Reinicie o aplicativo assim que possível,<br>para montagem precisa da interface.</html>", LAFInfo[index].getName());
          JOptionPane.showMessageDialog(getContentPane(),
            msg, "Satisfação ao Usuário", JOptionPane.INFORMATION_MESSAGE);
        }
      }
    }
  }

  /**
   * Calcula a espessura das bordas do Splitter no Look&Feel efetivo
   * essencial para o cálculo da posição corrigida do divisor de painéis.
   *
   * @return Espessura da borda em pixels.
  */
  private void getBorderWidth()
  {
    borderWidth = 0;
    Object o = UIManager.get("SplitPaneDivider.border");
    // checa se o componente esta definido no Look&Feel efetivo
    if ((o != null) && (o instanceof Border)) {
      Insets insets = ((Border) o).getBorderInsets(null);
      // checa se existe "canvas" para renderização
      if (insets != null) borderWidth = insets.left + insets.right;
    }
  }

  /**
   * Heurística para evitar a quebra do layout ao usar o método
   * split.resetToPreferredSizes.
  */
  private void adjustSplitter()
  {
    int pos = (11 * txPane.getPreferredSize().width) / 10 + borderWidth;
    split.setDividerLocation(pos);
  }

  /** Personaliza a interface declarando fontes, bordas e cores. */
  private void customizeUI()
  {
    // obtêm a fonte padrão
    Font font = (Font) UIManager.getDefaults().get(defFontName);
    float size = font.getSize2D();

    UIManager.put("swing.boldMetal", new Boolean(boldEnabled));

    if (boldEnabled) {
      UIManager.put("FormattedTextField.font", new FontUIResource(font.deriveFont(Font.BOLD)));
      UIManager.put("Table.font", new FontUIResource(font.deriveFont(Font.BOLD)));
    }

    UIManager.put("Button.font",
      new FontUIResource(font.deriveFont(Font.BOLD, (5 * size) / 6f)));
    UIManager.put("TitledBorder.font",
      new FontUIResource(font.deriveFont((31 * size) / 40f)));
    UIManager.put("ProgressBar.font",
      new FontUIResource(font.deriveFont(Font.BOLD)));
    UIManager.put("ToolTip.font", new FontUIResource(font));

    UIManager.put("TitledBorder.border",
      BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));
    UIManager.put("ToolTip.background", Color.decode("0xFFFFE0"));
    UIManager.put("SplitPane.dividerSize", new Integer(11));

    UIManager.put("OptionPane.yesButtonText", "Sim");
    UIManager.put("OptionPane.noButtonText", "Não");
    UIManager.put("OptionPane.cancelButtonText", "Cancelar");

    UIManager.put("FileChooser.lookInLabelMnemonic", new Integer('E'));
    UIManager.put("FileChooser.lookInLabelText", "Examinar");
    UIManager.put("FileChooser.saveInLabelText", "Salvar em:");
    UIManager.put("FileChooser.fileNameLabelMnemonic", new Integer('N'));
    UIManager.put("FileChooser.fileNameLabelText", "Nome do arquivo:");
    UIManager.put("FileChooser.filesOfTypeLabelMnemonic", new Integer('T'));
    UIManager.put("FileChooser.filesOfTypeLabelText", "Tipo de arquivo:");
    UIManager.put("FileChooser.upFolderToolTipText", "Um nível acima");
    UIManager.put("FileChooser.upFolderAccessibleName", "Um nível acima");
    UIManager.put("FileChooser.homeFolderToolTipText", "Desktop");
    UIManager.put("FileChooser.homeFolderAccessibleName", "Desktop");
    UIManager.put("FileChooser.newFolderToolTipText", "Criar nova pasta");
    UIManager.put("FileChooser.newFolderAccessibleName", "Criar nova pasta");
    UIManager.put("FileChooser.listViewButtonToolTipText", "Lista");
    UIManager.put("FileChooser.listViewButtonAccessibleName", "Lista");
    UIManager.put("FileChooser.detailsViewButtonToolTipText", "Detalhes");
    UIManager.put("FileChooser.detailsViewButtonAccessibleName", "Detalhes");
    UIManager.put("FileChooser.fileNameHeaderText", "Nome");
    UIManager.put("FileChooser.fileSizeHeaderText", "Tamanho");
    UIManager.put("FileChooser.fileTypeHeaderText", "Tipo");
    UIManager.put("FileChooser.fileDateHeaderText", "Data");
    UIManager.put("FileChooser.fileAttrHeaderText", "Atributos");
    UIManager.put("FileChooser.saveButtonText", "Salvar");
    UIManager.put("FileChooser.saveButtonToolTipText", "Salvar arquivo selecionado");
    UIManager.put("FileChooser.cancelButtonText", "Cancelar");
    UIManager.put("FileChooser.cancelButtonToolTipText", "Abortar esse dialogo");
    UIManager.put("FileChooser.openButtonText", "Abrir");
    UIManager.put("FileChooser.openButtonToolTipText", "Abrir arquivo selecionado");

    // somente para o Java5 que não reconhece "boldMetal"
    if (System.getProperty("java.version").startsWith("1.5")
        && !boldEnabled)
    {
      FontUIResource fuir = new FontUIResource(font.deriveFont(Font.PLAIN));
      UIManager.put("Label.font", fuir);
      UIManager.put("List.font", fuir);
      UIManager.put("ComboBox.font", fuir);
      UIManager.put("CheckBox.font", fuir);
      UIManager.put("RadioButton.font", fuir);
      UIManager.put("ToggleButton.font", fuir);
      UIManager.put("TextField.font", fuir);
      UIManager.put("PopupMenu.font", fuir);
      UIManager.put("Menu.font", fuir);
      UIManager.put("MenuItem.font", fuir);
      UIManager.put("MenuItem.acceleratorFont", fuir);
      UIManager.put("CheckBoxMenuItem.font", fuir);
      UIManager.put("RadioButtonMenuItem.font", fuir);
    }
  }

  /**
   * Construção padronizada da fonte usada nos labels do Slider.
   *
   * @param font Fonte de referência.
   * @return Fonte proporcional derivada da fonte de referência.
  */
  private Font deriveSliderFont(Font font)
  {
    return font.deriveFont((5 * font.getSize2D()) / 6f);
  }

  /**
   * Aumenta ou reduz efetivamente os tamanhos das fontes, redeclarando
   * os respectivos valores nos padrões do gestor da interface.
   *
   * @param increment Valor do incremento no tamanho das fontes.
  */
  private void changeUIFontSizes(float increment)
  {
    // obtêm os padrões default do gestor de interface
    UIDefaults defaults = UIManager.getDefaults();
    // atravessa a lista de chaves do gestor de interface escalando fontes
    for (Enumeration<?> keys=defaults.keys(); keys.hasMoreElements();)
    {
      Object key = keys.nextElement();
      // obtêm objeto associado à chave
      Object object = defaults.get(key);
      // checa se object não nulo é instância de Font
      if ((object != null) && (object instanceof Font))
      {
        Font font = (Font) object;
        // deriva fonte com tamanho escalado
        font = font.deriveFont(font.getSize2D() + increment);
        // sobrescreve o objeto associado à chave
        UIManager.put(key, new FontUIResource(font));
      }
    }
  }

  /**
   * Aumenta ou reduz os tamanhos das fontes da interface em 1 ponto.
   *
   * @param isToIncrement <b>true</b> aumenta e <b>false</b> reduz o tamanho
   *                      das fontes da interface em 1 ponto.
  */
  private void changeUIFontSizes(boolean isToIncrement)
  {
    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    float increment = isToIncrement ? 1f : -1f;
    changeUIFontSizes(increment);
    fontSize += increment;

    // cria uma fonte com tamanho proporcional à fonte default
    Font font = deriveSliderFont(
      (Font) UIManager.getDefaults().get(defFontName));
    // this cast is valid and no ClassCastException is thrown here
    @SuppressWarnings("unchecked")
    Dictionary<Integer,JLabel>
      h = (Hashtable<Integer,JLabel>) taxaSlider.getLabelTable();
    // percorre a lista de chaves enumeradas redeclarando
    // a fonte de cada componente associado a cada chave
    for (Enumeration<Integer> keys=h.keys(); keys.hasMoreElements();)
    {
      h.get(keys.nextElement()).setFont(font);
    }

    try {
      SwingUtilities.updateComponentTreeUI(this);
    } catch (Exception e) {
      System.err.println(e);
      try {
        SwingUtilities.updateComponentTreeUI(actionPane);
      } catch (Exception ex) {
        System.err.println(ex);
      }
    }

    // posiciona o divisor de painéis evitando quebra de layout
    adjustSplitter();

    updatePopupMenuUI(mainMenu);
    updatePopupMenuUI(editMenu);
    enableFontMenuItens();

    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

    if (warningsEnabled) {
      String msg = String.format("<html>O tamanho da fonte foi alterado para <b>%4.1f pts</b>.<br>Reinicie o aplicativo assim que possível, pois<br>essa dimensão é usada como referência na<br>montagem da interface.</html>", fontSize);
      JOptionPane.showMessageDialog(getContentPane(),
        msg, "Satisfação ao Usuário", JOptionPane.INFORMATION_MESSAGE);
    }
  }

  /** Habilita/Desabilita itens do submenu de gestão de tamanho de fonte. */
  private void enableFontMenuItens()
  {
    fontMenu.getItem(0).setEnabled(fontSize < maxFontSize);
    fontMenu.getItem(1).setEnabled(fontSize > minFontSize);
  }

  /** Alterna o estilo da fonte da interface. */
  private void changeUIFontStyle()
  {
    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    int style = boldEnabled ? Font.BOLD : Font.PLAIN;

    // obtêm os padrões default do gestor de interface
    UIDefaults defaults = UIManager.getDefaults();
    // atravessa a lista de chaves do gestor de interface escalando fontes
    for (Enumeration<?> keys=defaults.keys(); keys.hasMoreElements();)
    {
      Object key = keys.nextElement();
      // checa exceções de componentes
      if (key instanceof String) {
        String s = (String) key;
        if (s.equals("Button.font") || s.equals("ProgressBar.font")
            || s.equals("TitledBorder.font") || s.equals("ToolTip.font"))
        continue;
      }
      // obtêm objeto associado à chave
      Object object = defaults.get(key);
      // checa se object não nulo é instância de Font
      if ((object != null) && (object instanceof Font))
      {
        Font font = (Font) object;
        // deriva fonte com estilo
        font = font.deriveFont(style);
        // sobrescreve o objeto associado à chave
        UIManager.put(key, new FontUIResource(font));
      }
    }

    try {
      SwingUtilities.updateComponentTreeUI(this);
    } catch (Exception e) {
      System.err.println(e);
      try {
        SwingUtilities.updateComponentTreeUI(actionPane);
      } catch (Exception ex) {
        System.err.println(ex);
      }
    }

    // posiciona o divisor de painéis evitando quebra de layout
    adjustSplitter();

    updatePopupMenuUI(mainMenu);
    updatePopupMenuUI(editMenu);

    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

    if (warningsEnabled) {
      String msg = "<html>O estilo da fonte foi alterado.<br>Reinicie o aplicativo assim que possível, pois o estilo<br>altera a dimensão da fonte que é usada como<br>referência na montagem da interface.</html>";
      JOptionPane.showMessageDialog(getContentPane(),
        msg, "Satisfação ao Usuário", JOptionPane.INFORMATION_MESSAGE);
    }
  }

  /**
   * Obtêm valor correspondente à chave.
   *
   * @param p Conjunto de propriedades onde pesquisar.
   * @param key Chave de pesquisa.
   * @return A propriedade correspondente à chave ou uma string de
   *         comprimento zero se a chave não for encontrada.
  */
  protected String peek(Properties p, String key)
  {
    return p.getProperty(key, "");
  }

  /**
   * Carrega o arquivo de propriedades com o nome fornecido do diretório
   * de execução/instalação ou como arquivo resource.
   * Visando compatibilidade com Java 5, use codificação ISO-8859-1 nos
   * arquivos de propriedades.
   *
   * @param filename Nome do arquivo contendo as propriedades.
   * @return As propriedades desejadas.
  */
  protected Properties loadProperties(String filename)
  {
    Properties p = new Properties();
    InputStream in;
    File file = new File(USER_DIR + FILE_SEPARATOR + filename);
    try {
      if (file.exists()) in = new FileInputStream(file);
      else in = this.getClass().getResourceAsStream("META-INF/" + filename);
      p.load(in);
      in.close();
    } catch (IOException e) {
      System.err.println(e);
    }
    return p;
  }

  /**
   * Carrega as propriedades comportamentais mantendo-as quando possível
   * em tipos primitivos durante a execução do aplicativo.
   * Faz uso do único objeto Properties com escopo global e hereditário
   * para que descendentes também se beneficiem da persistência.
  */
  protected void loadBehavior()
  {
    Properties p = properties = loadProperties(BEHAVIOR_PROPERTIES);

    soundEnabled = peek(p, "SFX").equals("1");
    autoScaleEnabled = peek(p, "autoScale").equals("1");

    // obtem informações de Look&Feel disponíveis
    LAFInfo = UIManager.getInstalledLookAndFeels();

    // tenta restaurar o Look&Feel da sessão anterior
    String classname = peek(p, LAFPrefix);
    LAFIndex = -1;
    // checa o nome e disponibilidade do Look&Feel
    if (classname.length() > 0) {
      LAFIndex = LAFInfo.length - 1;
      while (LAFIndex >= 0
        && !LAFInfo[LAFIndex].getClassName().equals(classname)) LAFIndex--;
    }
    // se não declarado ou não encontrado então seleciona
    // o Look&Feel multiplataforma
    if (LAFIndex < 0) {
      classname = UIManager.getCrossPlatformLookAndFeelClassName();
      LAFIndex = 0;
      while (LAFIndex < LAFInfo.length
        && !LAFInfo[LAFIndex].getClassName().equals(classname)) LAFIndex++;
    }

    introductionText = peek(p, "introText");
    licenseText = peek(p, "licenseText");
    warningsEnabled = peek(p, "warnings").equals("1");

    toolTipManager = ToolTipManager.sharedInstance();
    toolTipManager.setEnabled(peek(p, "toolTip").equals("1"));

    lenientDatesEnabled = peek(p, "lenientDates").equals("1");

    try {
      prevX = Integer.parseInt(peek(p, "X"));
    } catch (NumberFormatException e) {
      prevX = 0;
    }

    try {
      prevY = Integer.parseInt(peek(p, "Y"));
    } catch (NumberFormatException e) {
      prevY = 0;
    }

    try {
      prevWidth = Integer.parseInt(peek(p, "width"));
    } catch (NumberFormatException e) {
      prevWidth = 0;
    }

    try {
      prevHeight = Integer.parseInt(peek(p, "height"));
    } catch (NumberFormatException e) {
      prevHeight = 0;
    }

    try {
      dividerLocation = Integer.parseInt(peek(p, "splitDivider"));
    } catch (NumberFormatException e) {
      dividerLocation = 0;
    }

    try {
      winState = Integer.parseInt(peek(p, "winState"));
    } catch (NumberFormatException e) {
      winState = Frame.NORMAL;
    }

    try {
      fontSize = Float.parseFloat(peek(p, "fontSize"));
    } catch (NumberFormatException e) {
      Font font = (Font) UIManager.getDefaults().get(defFontName);
      fontSize = font.getSize2D();
    } finally {
      if (fontSize < minFontSize) {
        fontSize = minFontSize;
      } else if (fontSize > maxFontSize) {
        fontSize = maxFontSize;
      }
    }

    boldEnabled = peek(p, "bold").equals("1");

    checkCAcerts = peek(p, "checkCAcerts").equals("1");
  }

  /**
   * Armazena um objeto Properties em arquivo do sistema.
   *
   * @param p Propriedades a serem armazenadas em arquivo.
   * @param filename Nome do arquivo.
  */
  protected void saveProperties(Properties p, String filename)
  {
    try {
      // compatível com o JRE5+
      FileOutputStream out = new FileOutputStream(filename);
      p.store(out, " NÃO EDITAR ESTE ARQUIVO!");
      out.flush();
      out.close();
    } catch (IOException e) {
      System.err.println(e);
    }
  }

  /**
   * Preserva as propriedades comportamentais mantidas quando possível em
   * tipos primitivos durante a execução do aplicativo.
   * Faz uso do único objeto Properties com escopo global e hereditário
   * para que descendentes também se beneficiem da persistência.
   * Nota: Esse método é invocado unicamente no fechamento da janela.
  */
  protected void saveBehavior()
  {
    Properties p = properties;

    p.put("SFX", soundEnabled ? "1" : "0");
    p.put("autoScale", autoScaleEnabled ? "1" : "0");
    p.put("warnings", warningsEnabled ? "1" : "0");
    p.put("toolTip", toolTipManager.isEnabled() ? "1" : "0");
    p.put("lenientDates", lenientDatesEnabled ? "1" : "0");
    p.put(LAFPrefix, UIManager.getLookAndFeel().getClass().getName());
    p.put("introText", introductionText);
    p.put("licenseText", licenseText);

    p.put("fontSize", String.format(Locale.US, "%f", fontSize));
    p.put("bold", boldEnabled ? "1" : "0");

    winState = getExtendedState();
    p.put("winState", String.format("%d", winState));

    if ((winState & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) {
      setExtendedState(Frame.NORMAL);
      // somente para postergar tarefas dependentes da alteração de estado
      int w = Toolkit.getDefaultToolkit().getScreenSize().width;
      try {
        do Thread.sleep(100L); while (getWidth() == w);
      } catch (InterruptedException e) {
        System.err.println(e);
      }
    }

    Point at = getLocationOnScreen();
    p.put("X", String.format("%d", at.x));
    p.put("Y", String.format("%d", at.y));

    p.put("width", String.format("%d", getWidth()));
    p.put("height", String.format("%d", getHeight()));

    p.put("splitDivider", String.format("%d", split.getDividerLocation() - borderWidth));

    p.put("checkCAcerts", checkCAcerts ? "1" : "0");

    saveProperties(p, BEHAVIOR_PROPERTIES);
  }

  /**
   * Carrega documento HTML de um arquivo comum ou como resource exibindo
   * o conteúdo no painel de reportagem.
   *
   * @param filename Nome do arquivo HTML.
  */
  private void loadNotes(String filename)
  {
    if (filename.length() > 0) {
      clearReport();
      report.getEditorKit().createDefaultDocument();
      report.setContentType("text/html");
      report.setText( loadText(filename).toString() );
      report.setCaretPosition(0);
      report.getParent().requestFocusInWindow();
    }
  }

  /**
   * Monta um buffer de strings com o conteúdo textual de um arquivo com
   * nome fornecido e codificação UTF-8.
   * Sempre tenta carregar o arquivo do diretório de execução/instalação
   * antes de carregar como arquivo resource.
   *
   * @param filename Nome do arquivo contendo o texto.
   * @return O buffer com o conteúdo do arquivo.
  */
  protected StringBuilder loadText(String filename)
  {
    StringBuilder sb = new StringBuilder();
    InputStream in;
    File file = new File(USER_DIR + FILE_SEPARATOR + filename);
    try {
      if (file.exists()) in = new FileInputStream(file);
      else in = this.getClass().getResourceAsStream("META-INF/" + filename);
      BufferedReader rd = new BufferedReader(
        new InputStreamReader(in, "UTF8"));
      String line;
      while ((line = rd.readLine()) != null)
      {
        if (sb.length() > 0) sb.append(CR);
        sb.append(line);
      }
      rd.close();
    } catch (IOException e) {
      System.err.println(e);
    }
    return sb;
  }

  /**
   * Executa o audioclip de índice fornecido se o uso estiver habilitado.
   *
   * @param index Índice do audioclip no array de audioclips.
  */
  protected void playAudioClip(int index)
  {
    if (soundEnabled && (audioclip != null) && (audioclip[index] != null))
    {
      audioclip[index].play();
    }
  }

  /**
   * Ajusta a posição do divisor de painéis garantindo a exibição
   * do painel a direita ou a esquerda.
   *
   * @param rigthSide <b>true</b> painel direito e <b>false</b> o esquerdo.
  */
  protected void assureSplitPaneDisplay(boolean rigthSide)
  {
    int div = split.getDividerLocation() - borderWidth;
    if (rigthSide) {
      int max = (2 * split.getMaximumDividerLocation() - split.getDividerSize() - borderWidth) / 3;
      if (div >= max) adjustSplitter();
    } else {
      int min = (2 * formPane.getPreferredSize().width) / 3;
      if (div <= min) adjustSplitter();
    }
  }

  /**
   * Obtêm a opção/botão selecionado de seu grupo de contrôle.
   *
   * @param group Grupo de contrôle objeto da operação.
   * @return Componente Selecionado.
  */
  protected AbstractButton getSelectedGroupOption(ButtonGroup group)
  {
    for (Enumeration<?> e = group.getElements(); e.hasMoreElements();) {
      AbstractButton b = (AbstractButton) e.nextElement();
      if (b.getModel() == group.getSelection())
        return b;
    }
    return null;
  }

  /**
   * Posiciona o diálogo no centro do frame.
   *
   * @param dialog O diálogo a centralizar.
  */
  protected void centerDialog(JDialog dialog)
  {
    dialog.pack();
    int x = getX() + (getWidth() - dialog.getWidth()) / 2;
    int y = getY() + (getHeight() - dialog.getHeight()) / 2;
    dialog.setLocation(x, y);
  }

  /** Limpa completamente o painel de reportagem: conteúdo e atributos. */
  protected void clearReport()
  {
    Document doc = report.getDocument();
    int len = doc.getLength();
    if (len > 0) {
      try {
        // remove text contents
        doc.remove(0, len);
      } catch (BadLocationException e) {
      }
      if (doc instanceof HTMLDocument) {
        StyleSheet css = ((HTMLDocument) doc).getStyleSheet();
        if (css != null) {
          // remove css styles
          Enumeration<?> list = css.getStyleNames();
          while (list.hasMoreElements()) {
            css.removeStyle((String) list.nextElement());
          }
        }
      }
    }
  }

  /** Diálogo de impressão do documento no painel de reportagem. */
  private boolean printReport()
  {
    playAudioClip(4);

    // checa disponibilidade do serviço de impressão padrão
    if (PrintServiceLookup.lookupDefaultPrintService() == null)
    {
      JOptionPane.showMessageDialog(getContentPane(),
        "Serviço de impressão padrão não configurado.",
        "Impressão de Documento", JOptionPane.ERROR_MESSAGE);
      return false;
    }

    HTMLDocument doc = (HTMLDocument) report.getDocument();
    DocumentRenderer renderer = new DocumentRenderer(
      String.format("JCM %1$td.%1$tm.%1$ty% (%1$tH:%1$tM)", System.currentTimeMillis()));
    renderer.print(doc);

    return true;
  }

  /** Diálogo ao usuário para salvar o documento no painel de reportagem. */
  private void saveReport()
  {
    JFileChooser fc = new JFileChooser(USER_DIR);
    fc.setDialogTitle("Salvar documento em arquivo");
    fc.setAcceptAllFileFilterUsed(false);
    fc.addChoosableFileFilter(new TxtFileFilter());
    fc.addChoosableFileFilter(new HTMLFileFilter());
    if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
      File file = fc.getSelectedFile();
      if (fc.getFileFilter() instanceof HTMLFileFilter) {
        saveHtmlReportToDisk(file);
      } else {
        saveTxtReportToDisk(file);
      }
    }
  }

  /**
   * Salva o documento no painel de reportagem como arquivo text/plain.
   *
   * @param file Arquivo destino do documento.
  */
  private void saveTxtReportToDisk(File file)
  {
    Document doc = report.getDocument();
    try {
      // create a segment to hold the characters in the document
      Segment segment = new Segment();
      segment.setPartialReturn(true);
      // open (or create) storage target file
      FileWriter fw = new FileWriter(file);
      // retrieve all segments
      for (int pos=0, doclen = doc.getLength(); pos < doclen;) {
        // ask for the remainder of the document text
        doc.getText(pos, doclen - pos, segment);
        // save retrieved segment to file
        fw.write(segment.array, pos, segment.count);
        // increment pos by the actual amount of characters retrieved
        pos += segment.count;
      }
      fw.flush(); // assures file updating
      fw.close();
    } catch (IOException e) {
      System.err.println(e);
    } catch (BadLocationException e) {
    }
  }

  /**
   * Salva o documento no painel de reportagem como arquivo text/html.
   *
   * @param file Arquivo destino do documento.
  */
  private void saveHtmlReportToDisk(File file)
  {
    try {
      // Save html document to a string buffer.
      HTMLDocument doc = (HTMLDocument) report.getDocument();
      EditorKit kit = report.getEditorKitForContentType("text/html");
      Writer writer = new StringWriter();
      kit.write(writer, doc, 0, doc.getLength());
      writer.flush();
      // writer.close();
      StringBuffer sb = ((StringWriter) writer).getBuffer();
      // Save enhanced string buffer to file.
      writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
      // add DTD for HTML 4.01 Transitional
      writer.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">" + CR);
      // insert a meta tag to specify document content type and charset
      int j = sb.indexOf("<style");
      if (j != -1) sb.insert(j, "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">" + CR);
      writer.write(sb.toString());
      writer.flush();
      writer.close();
    } catch (IOException ioe) {
      System.err.println(ioe);
    } catch (BadLocationException ble) {
      System.err.println(ble);
    }
  }

  /** Filtro de arquivos tipo text/plain. */
  private class TxtFileFilter extends FileFilter
  {
    public boolean accept(File file) {
      return file.getName().matches("(?iu:.+?\\.TXT)") || file.isDirectory();
    }
    public String getDescription() { return "Arquivos de Texto (*.txt)"; }
  }

  /** Filtro de arquivos tipo text/html. */
  private class HTMLFileFilter extends FileFilter
  {
    public boolean accept(File file) {
      return file.getName().matches("(?iu:.+?\\.HTML?)") || file.isDirectory();
    }
    public String getDescription() { return "Arquivos HTML (*.html;*.htm)"; }
  }

  /**
   * Parte do workaround para o bug do painel de calendário aberto quando
   * um componente JComboBox ganha o foco.
   *
   * Dado um componente JDateChooser fechará o painel se estiver aberto.
   *
   * @param  chooser Componente JDateChooser a checar.
   * @return Status da operação: <b>true</b> se estava aberto <b>false</b>
   *         em caso contrário.
  */
  private boolean foundOpenCalendar(JDateChooser chooser)
  {
    JPopupMenu popup = (JPopupMenu) chooser.getJCalendar().getParent();
    if (popup.isVisible()) {
      popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", true);
      popup.setVisible(false);
      return true;
    }
    return false;
  }

  /**
   * Ajusta o slider conforme valor fornecido.
   *
   * @param value Valor do slider ao final da operação.
  */
  private void adjustSlider(BigDecimal value)
  {
    taxaSlider.setValue( value.multiply(DEZMIL).intValue() );

    boolean valid = (value.compareTo(TAXA.getMAX()) < 1);

    if (taxaSlider.isEnabled() != valid) {
      // this cast is valid and no ClassCastException is thrown here
      @SuppressWarnings("unchecked")
      Dictionary<Integer,JLabel>
        h = (Hashtable<Integer,JLabel>) taxaSlider.getLabelTable();
      h.get(1200).setText(valid ? "12" : ">12");
      taxaSlider.updateUI();
      taxaSlider.setEnabled(valid);
    }
  }

  /***** LISTENERS CORNER *****/

  /** Listener especializado no evento "popup menu trigger". */
  private class TextFieldMouseAdapter extends MouseAdapter
  {
    /** Componente origem do evento popup do menu de edição.*/
    public Component originator;

    /**
     * Abre o menu popup conforme componente origem do evento.
     *
     * @param evt O evento "popup menu" observado.
    */
    private void openMenu(MouseEvent evt)
    {
      if (evt.getSource() instanceof JTextField) {
        // preserva o componente origem do evento
        originator = evt.getComponent();
        // força o foco no componente se necessário
        if (!originator.hasFocus()) originator.requestFocusInWindow();
        // abre o menu popup de edição aonde clicado
        editMenu.show(originator, evt.getX(), evt.getY());
      } else {
        playAudioClip(3);
        // abre o menu popup principal
        mainMenu.show(evt.getComponent(), evt.getX(), evt.getY());
      }
    }

    /**
     * Observador do evento "popup menu trigger".
     *
     * @param evt O evento observado.
    */
    @Override public void mousePressed(MouseEvent evt)
    {
      if (evt.isPopupTrigger()) openMenu(evt);
    }

    /**
     * Observador adicional do evento "popup menu trigger" visando
     * funcionalidade multiplataforma, conforme recomendado na documentação.
     *
     * @param evt O evento observado.
    */
    @Override public void mouseReleased(MouseEvent evt)
    {
      if (evt.isPopupTrigger()) openMenu(evt);
    }
  }

  /**
   * Observador de ações associadas a vários componentes.
   *
   * @param e O evento observado.
  */
  public void actionPerformed(ActionEvent e)
  {
    Object c = e.getSource();
    if (c instanceof JMenuItem) {
      JMenuItem mi = (JMenuItem) c;
      if (mi.getParent() == editMenu) {
        final JTextComponent t = (JTextComponent) popupListener.originator;
        String miName = mi.getName();
        if (miName.equals("COPY")) {
          t.copy();
        } else if (miName.equals("CUT")) {
          t.cut();
        } else if (miName.equals("PASTE")) {
          t.paste();
        } else if (miName.equals("SELECT_ALL")) {
          EventQueue.invokeLater( // bug (#4740914) fix
            new Runnable() { public void run() { t.selectAll(); } }
          );
        } else /* if (miName.matches("(RE|UN)DO")) */ {
          Object oo = t.getActionMap().get(miName);
          if (oo != null) {
            ((Action) oo).actionPerformed(
              new ActionEvent(t, ActionEvent.ACTION_PERFORMED, miName));
          }
        }
      } else {
        if (mi instanceof LAFMenuItem) {
          playAudioClip(8);
          setLAF( ((LAFMenuItem) mi).getIndex() );
        } else {
          String compoName = mi.getName();
          if (mi instanceof JCheckBoxMenuItem) {
            playAudioClip(4);
            boolean b;
            if (compoName.equals("SOUND")) {
              b = soundEnabled = !soundEnabled;
              if (b) {
                if (audioclip == null) loadAudioClips();
                playAudioClip(4);
              }
            } else if (compoName.equals("SCALE")) {
              b = autoScaleEnabled = !autoScaleEnabled;
            } else if (compoName.equals("BOLD")) {
              b = boldEnabled = !boldEnabled;
              changeUIFontStyle();
            } else if (compoName.equals("WARNINGS")) {
              b = warningsEnabled = !warningsEnabled;
            } else if (compoName.equals("CACERTS")) {
              b = checkCAcerts = !checkCAcerts;
            } else if (compoName.equals("LENIENT")) {
              b = lenientDatesEnabled = !lenientDatesEnabled;
            } else /*if (compoName.equals("TOOLTIP"))*/ {
              b = !toolTipManager.isEnabled();
              toolTipManager.setEnabled(b);
            }
            ((JCheckBoxMenuItem) mi).setState(b);
          } else {
            if (compoName.equals("DBREPORT")) {
              makeDbReport();
              playAudioClip(5);
            } else if (compoName.equals("UPDATE_TABLE")) {
              updateTable();
            } else if (compoName.equals("EDIT_TABLE")) {
              editTable();
            } else if (compoName.equals("CREATE_TABLE")) {
              createTable();
            } else if (compoName.equals("DROP_TABLE")) {
              dropTable();
            } else if (compoName.equals("TIPS")) {
              assureSplitPaneDisplay(true);
              loadNotes(introductionText);
              report.requestFocusInWindow();
              playAudioClip(5);
            } else if (compoName.equals("LIC")) {
              assureSplitPaneDisplay(true);
              loadNotes(licenseText);
              report.requestFocusInWindow();
              playAudioClip(5);
            } else if (compoName.equals("SHOW_RESULTS")) {
              split.setDividerLocation(0);
              playAudioClip(5);
            } else if (compoName.equals("SHOW_BOTH")) {
              split.setDividerLocation(dividerLocation + borderWidth);
              playAudioClip(5);
            } else if (compoName.equals("DECFONT")) {
              changeUIFontSizes(false);
            } else if (compoName.equals("INCFONT")) {
              changeUIFontSizes(true);
            } else if (compoName.equals("EXIT")) {
              getWindowListeners()[0].windowClosing(null);
              System.exit(0);
            }
          }
        }
      }
    } else {
      if (c == execBtn) {
        // esconde o menu de edição se necessário
        if (editMenu.isVisible()) editMenu.setVisible(false);
        // força encerramento de edição em campos formatados
        try {
          vini.commitEdit();
          vini.setText(
            vini.getFormatter().valueToString((Number) vini.getValue()));
          taxa.commitEdit();
          taxa.setText(
            taxa.getFormatter().valueToString((Number) taxa.getValue()));
        } catch (ParseException pe) {
          if (!vini.isEditValid()) {
            JOptionPane.showMessageDialog(getContentPane(),
              "<html><b>Valor</b> a atualizar mal declarado.</html>",
              "Validação de Dados", JOptionPane.ERROR_MESSAGE);
            vini.requestFocusInWindow();
          } else {
            JOptionPane.showMessageDialog(getContentPane(),
              "<html><b>Taxa de Juros</b> mal declarada.</html>",
              "Validação de Dados", JOptionPane.ERROR_MESSAGE);
            taxa.requestFocusInWindow();
          }
          return;
        }
        // posterga a execução dos cálculos visando atualização da UI
        EventQueue.invokeLater(new Runnable() {
          public void run() { makeSpreadsheet(); }
        });
      } else if (c == saveBtn) {
        saveReport();
      } else if (c == printBtn) {
        printReport();
      } else if (c == aboutBtn) {
        loadNotes(licenseText);
        if (aboutText == null) {
          String fmt = loadText("about.html").toString();
          // carrega informações sobre versão
          Properties p = loadProperties("build.info");
          // monta o texto a exibir no diálogo "about"
          aboutText = String.format(fmt, peek(p, "build.date"), peek(p, "build.number"));
        }
        JDialog about = new AboutDialog(this, aboutText);
        centerDialog(about);
        playAudioClip(11);
        about.setVisible(true);
        report.requestFocusInWindow();
      }
    }
  }

  /**
   * Variável lógica para evitar operações redundantes entre os quatro
   * componentes conjugados na edição da taxa de juros, período e regime.
  */
  private boolean ocupado = false;

  /**
   * Observador de modificações no slider.
   *
   * @param ce Evento de modificação observado.
  */
  public void stateChanged(ChangeEvent ce)
  {
    if (ocupado) return;
    ocupado = true;
    int ticks = taxaSlider.getValue();
    BigDecimal value = (new BigDecimal(ticks)).divide(InterestRate.HUNDRED, mc);
    taxa.setValue(value);
    TAXA.setValor(value.divide(InterestRate.HUNDRED, mc));
    if ((ticks % 5) == 0) playAudioClip(7);
    //System.out.println("SLIDER: " + TAXA.getValor());
    ocupado = false;
  }

  /**
   * Observador de trocas de estado em itens de componentes JRadioButton
   * e de componentes JComboBox.
   *
   * @param e Evento de troca de estado em itens observado.
  */
  public void itemStateChanged(ItemEvent e)
  {
    if (e.getStateChange() == ItemEvent.SELECTED)
    {
      playAudioClip(4);
      Object c = e.getSource();
      if (c == opSimples || c == opComposta)
      {
        boolean regime = (c == opSimples);
        if (autoScaleEnabled)
        {
          ocupado = true;
          TAXA.setValor(TAXA.eqValue(regime));
          TAXA.setRegime(regime);
          if (TAXA.scaleOnGreaterThanMAX())
          {
            periodCombo.setSelectedIndex(TAXA.getPeriodo());
          }
          taxa.setValue(TAXA.getValor().multiply(InterestRate.HUNDRED));
          adjustSlider(TAXA.getValor());
          //System.out.println("REGIME: " + TAXA.getValor());
          ocupado = false;
        } else {
          TAXA.setRegime(regime);
        }
      }
      else if (c == periodCombo)
      {
        if (ocupado) return;
        if (autoScaleEnabled)
        {
          ocupado = true;
          TAXA.setValor(TAXA.eqValue(periodCombo.getSelectedIndex()));
          TAXA.setPeriodo(periodCombo.getSelectedIndex());
          if (TAXA.scaleOnGreaterThanMAX())
          {
            periodCombo.setSelectedIndex(TAXA.getPeriodo());
          }
          taxa.setValue(TAXA.getValor().multiply(InterestRate.HUNDRED));
          adjustSlider(TAXA.getValor());
          //System.out.println("PERIOD: " + TAXA.getValor());
          ocupado = false;
        } else {
          TAXA.setPeriodo(periodCombo.getSelectedIndex());
        }
      }
      else if (c == indicesCombo)
      {
        updateIndicesTipText();
      }
    }
  }

  /**
   * Observador de alterações em propriedades do campo taxa e do splitter.
   *
   * @param ce Evento de alteração de propriedades observado.
  */
  public void propertyChange(PropertyChangeEvent ce)
  {
    if (ce.getSource() == taxa) {
      if (ocupado) return;
      double d = ((Number) taxa.getValue()).doubleValue();
      BigDecimal value = (new BigDecimal(d)).divide(InterestRate.HUNDRED, mc);
      //System.out.println("taxa: " + value);
      TAXA.setValor(value);
      ocupado = true;
      if (TAXA.scaleOnGreaterThanMAX())
      {
        taxa.setValue(TAXA.getValor().multiply(InterestRate.HUNDRED));
        periodCombo.setSelectedIndex(TAXA.getPeriodo());
      }
      adjustSlider(TAXA.getValor());
      ocupado = false;
    } else {
      if (ce.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY))
      {
        // posição corrigida do divisor de painéis
        int div = split.getDividerLocation() - borderWidth;
        // posição mínima do divisor de painéis
        int min = split.getMinimumDividerLocation() - borderWidth;
        // posição máxima do divisor de painéis
        int max = split.getMaximumDividerLocation() - split.getDividerSize() - borderWidth;
        // checa se o divisor não está num dos extremos
        boolean notAtExtremes = (min < div && div < max);

        // habilita/desabilita opções do submenu do splitter
        // conforme posição atual do divisor de painéis
        // SHOW_REPORTS
        splitMenu.getItem(0).setEnabled(div > min);
        // SHOW_BOTH
        splitMenu.getItem(1).setEnabled(! notAtExtremes);

        // altera a posição preferêncial do divisor de painéis
        // a menos que a posição candidata esteja nos extremos
        if (notAtExtremes) dividerLocation = div;
      }
    }
  }

  /**
   * Observador do foco adquirido no report e em componentes JComboBox.
   * É o método que incrementa a acessibilidade do painel de reportagem
   * e é parte do workaround para o bug do calendário aberto quando
   * um componente JComboBox ganha foco.
   *
   * @param fe Evento foco adquirido/ganho observado.
  */
  public void focusGained(FocusEvent fe)
  {
    if (fe.getSource() == report) {
      report.setBorder(BorderFactory.createLineBorder(Color.lightGray, 1));
    } else {
      // Part of the workaround to the bug of open calendar panel when a
      // JComboBox gains focus.
      if (foundOpenCalendar(iniChooser) || foundOpenCalendar(fimChooser))
      {
        final JComboBox c = (JComboBox) fe.getSource();
        EventQueue.invokeLater(new Runnable() {
          public void run() { c.setPopupVisible(true); }
        });
      }
    }
  }

  /**
   * Contra-parte do método anterior para o painel de reportagem quando
   * perde o foco.
   *
   * @param fe Evento foco perdido.
  */
  public void focusLost(FocusEvent fe)
  {
    if (fe.getSource() == report) report.setBorder(
      BorderFactory.createLineBorder(Color.decode("0xFDFDFD"), 1));
  }

  /***** Wanna Be! *****/

  /** Inicia a conexão ao DB. */
  protected abstract void dbConnect();

  /** Encerra a conexão ao DB. */
  protected abstract void dbCloseConnection();

  /** Atualiza o texto de ajuda contextual extraído do DB */
  protected abstract void updateIndicesTipText();

  /** Abre diálogo de criação de tabela do DB. */
  protected abstract void createTable();

  /** Abre diálogo de eliminação de tabela do DB. */
  protected abstract void dropTable();

  /** Abre diálogo de edição da tabela do DB selecionada. */
  protected abstract void editTable();

  /** Atualização online da tabela do DB selecionada. */
  protected abstract void updateTable();

  /** Monta um documento reportando o status operacional do DB. */
  protected abstract void makeDbReport();

  /**
   * Executa a atualização monetária e exibe o documento contendo a
   * planilha no painel de reportagem.
   *
   * @return O status da operação.
  */
  protected abstract boolean makeSpreadsheet();

  /** Propriedades de escopo global e hereditário. */
  protected Properties properties;

  /** Dispositivo gerenciador da ajuda contextual. */
  private ToolTipManager toolTipManager;

  /** Lista de gestores de histórico de edição de campos textuais. */
  private Hashtable<String,UndoManager> undoManagerList;

  /** Posição horizontal do frame na sessão anterior. */
  private int prevX;

  /** Posição vertical do frame na sessão anterior. */
  private int prevY;

  /** Largura do frame na sessão anterior */
  private int prevWidth;

  /** Altura do frame na sessão anterior */
  private int prevHeight;

  /** Espessura das bordas do Splitter no Look&Feel efetivo. */
  private int borderWidth;

  /** Posição do divisor de painéis na sessão anterior. */
  private int dividerLocation;

  /** Estado extendido do frame na sessão anterior. */
  private int winState;

  /** Tamanho efetivo da fonte da interface em pontos. */
  private float fontSize;

  /** Indicador de uso de estilo negrito nas fontes da interface. */
  private boolean boldEnabled;

  /** Indicador do uso de taxas equivalentes. */
  private boolean autoScaleEnabled;

  /** Indicador do uso de audioclips em eventos. */
  private boolean soundEnabled;

  /** Indicador do uso de notificações ao usuário. */
  protected boolean warningsEnabled;

  /** Indicador do uso de datas lenientes nas datas de agregação de juros.*/
  protected boolean lenientDatesEnabled;

  /** Indicador de verificação do arquivo de certificados do JSSE na inicialização. */
  protected boolean checkCAcerts;

  /** Nome do arquivo resource contendo o texto de introdução. */
  private String introductionText;

  /** Nome do arquivo resource contendo o texto da licença de uso. */
  private String licenseText;

  protected JTextFieldDoubleEditor vini;

  protected JDateChooser iniChooser;

  protected JDateChooser fimChooser;

  private JSlider taxaSlider;

  private JTextFieldDoubleEditor taxa;

  private JComboBox periodCombo;

  protected ButtonGroup regimeGroup;

  private JRadioButton opSimples;

  private JRadioButton opComposta;

  protected ButtonGroup agregacaoGroup;

  private JRadioButton opSemestral;

  protected JRadioButton opAnual;

  protected JComboBox indicesCombo;

  protected JProgressBar progressBar;

  private JButton aboutBtn;

  private JButton saveBtn;

  private JButton printBtn;

  private JButton execBtn;

  /** Container do formulário e painel de reportagem. */
  private JSplitPane split;

  /** Painel do formulário. */
  private JPanel formPane;

  /** Container do painel de reportagem. */
  private JScrollPane displayPane;

  /** Painel de reportagem. */
  protected JTextPane report;

  /** Painel de input da taxa de juros. */
  private JPanel txPane;

  /** Painel dos botões e da barra de progresso. */
  private JPanel actionPane;

  /** Menu popup principal. */
  private JPopupMenu mainMenu;

  /** Menu popup de edição. */
  private JPopupMenu editMenu;

  /** Submenu de gerenciamento da exibição dos painéis. */
  private JMenu splitMenu;

  /** Submenu de seleção do Look&Feel.*/
  private JMenu LAFmenu;

  /** Submenu de gestão do tamanho de fonte. */
  private JMenu fontMenu;

  /** Array de informações dos Look&Feel instalados. */
  private UIManager.LookAndFeelInfo[] LAFInfo;

  /** Índice do Look&Feel usado na sessão anterior. */
  private int LAFIndex;

  /** Listener específico de campos de texto. */
  private TextFieldMouseAdapter popupListener;

  /** Array dos audioclips. */
  private AudioClip audioclip[];

  /** Gerenciador absoluto da taxa de juros. */
  protected InterestRate TAXA;

  /** Texto a respeito desse aplicativo. */
  private String aboutText;

  /** Separador de linhas do sistema operacional. */
  private final String CR = System.getProperty("line.separator");

  /** Separador de arquivos do sistema operacional. */
  public final String FILE_SEPARATOR = System.getProperty("file.separator");

  /** Diretório de instalação/execução desse aplicativo. */
  public final String USER_DIR = System.getProperty("user.dir");

  /** Tamanho mínimo de fonte da interface em pontos. */
  private static final float minFontSize = 11f;

  /** Tamanho máximo de fonte da interface em pontos. */
  private static final float maxFontSize = 16f;

  /** Chave do componente default no gestor de interface. */
  private static final String defFontName = "Panel.font";

  /** Contexto matemático para operações com BigDecimal. */
  protected static final MathContext mc = MathContext.DECIMAL64;

  /** Constante de escala usada na interface da taxa de juros. */
  private static final BigDecimal DEZMIL = new BigDecimal("1E4");

  /** Prefixo do Look&Feel de uso geral. */
  private static final String LAFPrefix = "LAF";

  /** Nome permanente do arquivo de propriedades comportamentais */
  private static final String BEHAVIOR_PROPERTIES = "behavior.properties";
}